"""
The Layer class represents a layer of neurons that is fully connected to the layer before it.
"""

import numpy as np

class Layer:

    def __init__(self, prev_layer_size, this_layer_size, weights=None, biases=None):
        """
        Store the sizes of the previous layer and this one, and initialize
        the weights that connect this later to the previous one and the
        biases for this layer. Weights and biases can be passed in; if not,
        they are initialized randomly.
        """
        self.prev_layer_size = prev_layer_size
        self.this_layer_size = this_layer_size

        # Initialize weights and biases, which for now will just be drawn from
        # a normal distribution with mean 0 and standard deviation 0.1, but
        # later we'll do this better.
        if weights is not None:
            self.weights = weights
        else:
            self.weights = np.random.randn(this_layer_size, prev_layer_size) * 0.1
        if biases is not None:
            self.biases = biases
        else:
            self.biases = np.random.randn(this_layer_size) * 0.1

        # reshape into a column matrix for batches
        self.biases = self.biases.reshape(-1, 1)

    def forward_pass(self, batch_inputs):
        """
        Compute the value for the neurons in this layer given the input
        from the previous layer.
        """
        self.inputs = batch_inputs
        return np.dot(self.weights, batch_inputs) + self.biases
    
    def backward_pass(self, dvalues):
        """
        Compute the gradient of the loss with respect to the weights and biases
        feeding into this layer and the neuron values of previous layer (aka, the
        inputs into this layer). Input is the gradient with respect to neuron values
        in this layer. Think of this like we are pushing out the gradients from us
        into the previous layer.
        """
        # Compute the gradient of the loss with respect to the weights and biases
        self.dweights = np.dot(dvalues, self.inputs.T)
        self.dbiases = np.sum(dvalues, axis=1, keepdims=True)

        # Compute the gradient of the loss with respect to the inputs
        self.dinputs = np.dot(self.weights.T, dvalues)
        return self.dinputs
